Conversation
📝 WalkthroughWalkthroughThis PR introduces iOS App Intents support for Apple Wallet automation, enabling transaction logging via Shortcuts. It adds new Swift intent classes, React Native bridge modules, a settings UI component, and an Expo configuration plugin. The app version is bumped to 1.1.1, and the iOS deployment target is raised to 15.1. Changes
Sequence Diagram(s)sequenceDiagram
participant User as User
participant ShortcutsApp as iOS Shortcuts
participant ShortcutsSetup as ShortcutsSetupSheet
participant ReactBridge as ShortcutsModule<br/>(React Native Bridge)
participant LogIntent as LogTransactionIntent<br/>(App Intent)
participant Helper as IntentDatabaseHelper
participant SQLite as SQLite Database
participant HTTP as Exchange Rate API
User->>ShortcutsSetup: Opens Settings → Integrations
ShortcutsSetup->>User: Shows setup instructions & preview
User->>ShortcutsSetup: Taps "Test It" button
ShortcutsSetup->>ReactBridge: Calls testLogTransaction()
ReactBridge->>LogIntent: Trigger App Intent with amount & merchant
LogIntent->>Helper: Parse amount & read base currency
Helper->>SQLite: Read defaultCurrency from local_storage
SQLite-->>Helper: Return base currency (e.g., "USD")
Helper-->>LogIntent: Parsed amount, currency, base currency
alt Currency Conversion Needed
LogIntent->>Helper: Fetch exchange rate (from → to base)
Helper->>HTTP: Request exchange rate (5s timeout)
HTTP-->>Helper: Return rate or nil
Helper-->>LogIntent: Exchange rate
end
LogIntent->>Helper: insertTransaction(id, merchant, amount, ...)
Helper->>SQLite: INSERT into transactions table
SQLite-->>Helper: Return success/failure
Helper-->>LogIntent: success boolean
LogIntent-->>ReactBridge: Return success/error result
ReactBridge-->>ShortcutsSetup: Resolve/reject promise
ShortcutsSetup->>User: Show success/error alert
Estimated code review effort🎯 4 (Complex) | ⏱️ ~75 minutes Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 5
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
package.json (1)
4-4:⚠️ Potential issue | 🟡 MinorVersion mismatch:
package.jsonis still at1.1.0whileapp.json,android/app/build.gradle, andios/Info.plistare at1.1.1.Per the versioning procedure documented in
RELEASE_GUIDE.md(lines 33-40), bothapp.json -> expo.versionandpackage.json -> versionmust be bumped together.Proposed fix
- "version": "1.1.0", + "version": "1.1.1",🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@package.json` at line 4, Update the package.json "version" field from 1.1.0 to 1.1.1 so it matches app.json (expo.version), android/ios manifests and follows the RELEASE_GUIDE; ensure the package.json "version" entry in the root JSON is bumped to 1.1.1 and commit the change together with other versioned files.
🧹 Nitpick comments (1)
components/sheet/ShortcutsSetupSheet.tsx (1)
28-40: Remove inline comments.As per coding guidelines, comments should not be written in code for files matching
**/*.{js,jsx,ts,tsx}.♻️ Proposed fix
-// Previews shown inline after the step they illustrate (0-indexed). -// IMG_3788: the finished automation (trigger + Log Transaction action). -// IMG_3789: Log Transaction action with Amount / Merchant variables mapped. const STEP_PREVIEWS: Record<number, { source: number; caption: string }> = {🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@components/sheet/ShortcutsSetupSheet.tsx` around lines 28 - 40, Remove the inline comment block above STEP_PREVIEWS (the three comment lines describing previews and IMG filenames) so the file has no inline comments per project guidelines; locate the constant STEP_PREVIEWS and delete the leading comment lines that precede it (the lines referencing previews and IMG_3788/IMG_3789), leaving only the const declaration and its object literal intact, and scan the rest of ShortcutsSetupSheet.tsx for any other inline comments to remove similarly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@components/sheet/ShortcutsSetupSheet.tsx`:
- Around line 59-76: The Linking.openURL call in handleTestPress is executed
before the isTesting guard, so the URL will open even when a test is already
running; move the Linking.openURL invocation to after the isTesting check (or
better, after a successful test) so it only opens when appropriate.
Specifically, in handleTestPress ensure you first check and return early on
isTesting, then call setIsTesting(true), run
NativeModules.ShortcutsModule.testLogTransaction(), and only upon success call
Linking.openURL(...) and Alert.alert(...) before finally setIsTesting(false).
In `@ios/Stroberi/Intents/IntentDatabaseHelper.swift`:
- Around line 207-250: fetchExchangeRate is blocking the App Intent thread by
using DispatchSemaphore inside fetchExchangeRate() (called from
LogTransactionIntent.perform()); change fetchExchangeRate(from:to:) to be async
-> Double? and update callers (e.g., LogTransactionIntent.perform()) to await
it, replace the semaphore/dataTask pattern with URLSession.shared.data(for:
URLRequest) in an async loop over the two URL strings, keep the same
timeoutInterval on the URLRequest, parse the JSON and return the first valid
rate (or nil) — remove all DispatchSemaphore usage and dataTask closures so the
intent thread is not blocked.
In `@ios/Stroberi/Intents/LogTransactionIntent.swift`:
- Around line 42-52: The current else branch in LogTransactionIntent.swift
assigns exchangeRate = 1.0 and amountInBaseCurrency = expenseAmount when
IntentDatabaseHelper.fetchExchangeRate(...) returns nil; instead, do NOT persist
a fake base amount — remove the 1.0/expenseAmount assignments and either (A) set
exchangeRate and amountInBaseCurrency to a clear sentinel (e.g., nil or
Double.nan) and keep conversionStatus = "missing_rate" so the JS layer can
exclude it from base-currency calculations, or (B) abort/return a failed intent
response from the intent handler so the transaction is not saved; update the
code around IntentDatabaseHelper.fetchExchangeRate, exchangeRate,
amountInBaseCurrency and conversionStatus accordingly.
In `@lib/format.ts`:
- Around line 15-17: Remove the inline comment block that begins with
"Intl.NumberFormat throws on unsupported/malformed currency codes." and the
following two lines in lib/format.ts so the TypeScript source contains no inline
comments per repo rules; leave the code logic unchanged and ensure only the
comment lines are deleted around the Intl.NumberFormat-related note.
In `@plugins/intents/IntentDatabaseHelper.swift`:
- Around line 1-2: The template IntentDatabaseHelper.swift still imports SQLite3
and uses raw sqlite3_* calls; remove that direct SQLite usage and switch to the
IntentSQLiteBridge API (call its exposed methods instead of
sqlite3_open/sqlite3_prepare_v2/sqlite3_step/sqlite3_finalize/sqlite3_close) so
the plugin uses the same bridged implementation as the iOS helper; update the
template functions (e.g., any open/prepare/execute/query helpers) to invoke
IntentSQLiteBridge methods and/or consolidate this file with the fixed helper
used by the app (so plugins/withAppIntents.js copies the bridge-based source),
ensuring no direct SQLite3 import or sqlite3_* symbols remain in
IntentDatabaseHelper.swift.
---
Outside diff comments:
In `@package.json`:
- Line 4: Update the package.json "version" field from 1.1.0 to 1.1.1 so it
matches app.json (expo.version), android/ios manifests and follows the
RELEASE_GUIDE; ensure the package.json "version" entry in the root JSON is
bumped to 1.1.1 and commit the change together with other versioned files.
---
Nitpick comments:
In `@components/sheet/ShortcutsSetupSheet.tsx`:
- Around line 28-40: Remove the inline comment block above STEP_PREVIEWS (the
three comment lines describing previews and IMG filenames) so the file has no
inline comments per project guidelines; locate the constant STEP_PREVIEWS and
delete the leading comment lines that precede it (the lines referencing previews
and IMG_3788/IMG_3789), leaving only the const declaration and its object
literal intact, and scan the rest of ShortcutsSetupSheet.tsx for any other
inline comments to remove similarly.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 0d1ebe85-3074-4439-bc70-186862a64589
⛔ Files ignored due to path filters (8)
assets/images/ios_shortcute/IMG_3788-portrait.pngis excluded by!**/*.pngassets/images/ios_shortcute/IMG_3788.pngis excluded by!**/*.pngassets/images/ios_shortcute/IMG_3789-portrait.pngis excluded by!**/*.pngassets/images/ios_shortcute/IMG_3789.pngis excluded by!**/*.pngios/Podfile.lockis excluded by!**/*.lockios/Stroberi/Images.xcassets/SplashScreen.imageset/image.pngis excluded by!**/*.pngios/Stroberi/Images.xcassets/SplashScreenBackground.imageset/image.pngis excluded by!**/*.pngyarn.lockis excluded by!**/yarn.lock,!**/*.lock
📒 Files selected for processing (35)
.gitignoreandroid/app/build.gradleapp.jsonapp/(tabs)/analytics.tsxapp/(tabs)/settings.tsxcomponents/home/BudgetAlertCard.tsxcomponents/sheet/ErrorSheet.tsxcomponents/sheet/ImportCSVSheet.tsxcomponents/sheet/ShortcutsSetupSheet.tsxdatabase/actions/budgets.tsdatabase/actions/trips.tsios/Podfileios/Podfile.properties.jsonios/Stroberi.xcodeproj/project.pbxprojios/Stroberi.xcworkspace/xcshareddata/IDEWorkspaceChecks.plistios/Stroberi/Images.xcassets/SplashScreen.imageset/Contents.jsonios/Stroberi/Images.xcassets/SplashScreenBackground.imageset/Contents.jsonios/Stroberi/Info.plistios/Stroberi/Intents/IntentDatabaseHelper.swiftios/Stroberi/Intents/IntentSQLiteBridge.hios/Stroberi/Intents/IntentSQLiteBridge.mios/Stroberi/Intents/LogTransactionIntent.swiftios/Stroberi/Intents/ShortcutsModule.mios/Stroberi/Intents/ShortcutsModule.swiftios/Stroberi/SplashScreen.storyboardios/Stroberi/Stroberi-Bridging-Header.hlib/budgetAlerts.test.tslib/format.tspackage.jsonpatches/expo-localization+16.0.1.patchplugins/intents/IntentDatabaseHelper.swiftplugins/intents/LogTransactionIntent.swiftplugins/intents/ShortcutsModule.mplugins/intents/ShortcutsModule.swiftplugins/withAppIntents.js
💤 Files with no reviewable changes (3)
- ios/Stroberi.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
- ios/Stroberi/Images.xcassets/SplashScreen.imageset/Contents.json
- ios/Stroberi/Images.xcassets/SplashScreenBackground.imageset/Contents.json
| const handleTestPress = useCallback(async () => { | ||
| Linking.openURL('https://www.icloud.com/shortcuts/2d1a19f1410a43bda0fc283c46c84520') | ||
| if (isTesting) return; | ||
| setIsTesting(true); | ||
| try { | ||
| const { ShortcutsModule } = NativeModules; | ||
| if (!ShortcutsModule) { | ||
| Alert.alert('Unavailable', 'Shortcuts module is not available on this device.'); | ||
| return; | ||
| } | ||
| await ShortcutsModule.testLogTransaction(); | ||
| Alert.alert('Success', 'Test transaction created! Check your transaction list.'); | ||
| } catch { | ||
| Alert.alert('Error', 'Failed to create test transaction.'); | ||
| } finally { | ||
| setIsTesting(false); | ||
| } | ||
| }, [isTesting]); |
There was a problem hiding this comment.
URL opens before the isTesting guard check.
Linking.openURL is called unconditionally on line 60 before the isTesting early return on line 61. If the user taps "Test It" while already testing, the URL will still open. Consider moving the URL open to after the test completes successfully, or after the guard check.
🐛 Proposed fix - open URL after guard check
const handleTestPress = useCallback(async () => {
- Linking.openURL('https://www.icloud.com/shortcuts/2d1a19f1410a43bda0fc283c46c84520')
if (isTesting) return;
setIsTesting(true);
try {
const { ShortcutsModule } = NativeModules;
if (!ShortcutsModule) {
Alert.alert('Unavailable', 'Shortcuts module is not available on this device.');
return;
}
await ShortcutsModule.testLogTransaction();
Alert.alert('Success', 'Test transaction created! Check your transaction list.');
+ Linking.openURL('https://www.icloud.com/shortcuts/2d1a19f1410a43bda0fc283c46c84520');
} catch {
Alert.alert('Error', 'Failed to create test transaction.');
} finally {
setIsTesting(false);
}
}, [isTesting]);📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const handleTestPress = useCallback(async () => { | |
| Linking.openURL('https://www.icloud.com/shortcuts/2d1a19f1410a43bda0fc283c46c84520') | |
| if (isTesting) return; | |
| setIsTesting(true); | |
| try { | |
| const { ShortcutsModule } = NativeModules; | |
| if (!ShortcutsModule) { | |
| Alert.alert('Unavailable', 'Shortcuts module is not available on this device.'); | |
| return; | |
| } | |
| await ShortcutsModule.testLogTransaction(); | |
| Alert.alert('Success', 'Test transaction created! Check your transaction list.'); | |
| } catch { | |
| Alert.alert('Error', 'Failed to create test transaction.'); | |
| } finally { | |
| setIsTesting(false); | |
| } | |
| }, [isTesting]); | |
| const handleTestPress = useCallback(async () => { | |
| if (isTesting) return; | |
| setIsTesting(true); | |
| try { | |
| const { ShortcutsModule } = NativeModules; | |
| if (!ShortcutsModule) { | |
| Alert.alert('Unavailable', 'Shortcuts module is not available on this device.'); | |
| return; | |
| } | |
| await ShortcutsModule.testLogTransaction(); | |
| Alert.alert('Success', 'Test transaction created! Check your transaction list.'); | |
| Linking.openURL('https://www.icloud.com/shortcuts/2d1a19f1410a43bda0fc283c46c84520'); | |
| } catch { | |
| Alert.alert('Error', 'Failed to create test transaction.'); | |
| } finally { | |
| setIsTesting(false); | |
| } | |
| }, [isTesting]); |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@components/sheet/ShortcutsSetupSheet.tsx` around lines 59 - 76, The
Linking.openURL call in handleTestPress is executed before the isTesting guard,
so the URL will open even when a test is already running; move the
Linking.openURL invocation to after the isTesting check (or better, after a
successful test) so it only opens when appropriate. Specifically, in
handleTestPress ensure you first check and return early on isTesting, then call
setIsTesting(true), run NativeModules.ShortcutsModule.testLogTransaction(), and
only upon success call Linking.openURL(...) and Alert.alert(...) before finally
setIsTesting(false).
| static func fetchExchangeRate( | ||
| from targetCurrency: String, | ||
| to baseCurrency: String | ||
| ) -> Double? { | ||
| let target = targetCurrency.lowercased() | ||
| let base = baseCurrency.lowercased() | ||
|
|
||
| let urls = [ | ||
| "https://cdn.jsdelivr.net/npm/@fawazahmed0/currency-api@latest/v1/currencies/\(target).json", | ||
| "https://currency-api.pages.dev/v1/currencies/\(target).json" | ||
| ] | ||
|
|
||
| for urlString in urls { | ||
| guard let url = URL(string: urlString) else { continue } | ||
|
|
||
| let semaphore = DispatchSemaphore(value: 0) | ||
| var resultRate: Double? | ||
|
|
||
| var request = URLRequest(url: url) | ||
| request.timeoutInterval = 5 | ||
|
|
||
| let task = URLSession.shared.dataTask(with: request) { data, response, error in | ||
| defer { semaphore.signal() } | ||
| guard error == nil, | ||
| let httpResponse = response as? HTTPURLResponse, | ||
| httpResponse.statusCode == 200, | ||
| let data = data, | ||
| let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any], | ||
| let currencyData = json[target] as? [String: Any], | ||
| let rate = currencyData[base] as? Double, | ||
| rate > 0, rate.isFinite else { | ||
| return | ||
| } | ||
| resultRate = rate | ||
| } | ||
| task.resume() | ||
| semaphore.wait() | ||
|
|
||
| if let rate = resultRate { | ||
| return rate | ||
| } | ||
| } | ||
|
|
||
| return nil |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
find . -type f -name "IntentDatabaseHelper.swift" -o -name "LogTransactionIntent.swift"Repository: stroberi-app/stroberi
Length of output: 254
🏁 Script executed:
wc -l ios/Stroberi/Intents/IntentDatabaseHelper.swiftRepository: stroberi-app/stroberi
Length of output: 116
🏁 Script executed:
sed -n '207,250p' ios/Stroberi/Intents/IntentDatabaseHelper.swiftRepository: stroberi-app/stroberi
Length of output: 1662
🏁 Script executed:
cat -n ios/Stroberi/Intents/LogTransactionIntent.swift | head -100Repository: stroberi-app/stroberi
Length of output: 4034
🏁 Script executed:
rg "fetchExchangeRate" ios/Stroberi/Intents/Repository: stroberi-app/stroberi
Length of output: 264
Avoid blocking the App Intent thread during rate lookup.
LogTransactionIntent.perform() is async but calls fetchExchangeRate() synchronously, which blocks on a semaphore for each of the two URLs (lines 223 and 243). In slow or failing network conditions, this ties up the intent thread for ~10 seconds and can cause Shortcut execution to timeout. Refactor fetchExchangeRate() to be async and use URLSession.data(for:) instead of DispatchSemaphore.wait().
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@ios/Stroberi/Intents/IntentDatabaseHelper.swift` around lines 207 - 250,
fetchExchangeRate is blocking the App Intent thread by using DispatchSemaphore
inside fetchExchangeRate() (called from LogTransactionIntent.perform()); change
fetchExchangeRate(from:to:) to be async -> Double? and update callers (e.g.,
LogTransactionIntent.perform()) to await it, replace the semaphore/dataTask
pattern with URLSession.shared.data(for: URLRequest) in an async loop over the
two URL strings, keep the same timeoutInterval on the URLRequest, parse the JSON
and return the first valid rate (or nil) — remove all DispatchSemaphore usage
and dataTask closures so the intent thread is not blocked.
| } else if let rate = IntentDatabaseHelper.fetchExchangeRate( | ||
| from: transactionCurrency, to: baseCurrency | ||
| ) { | ||
| exchangeRate = rate | ||
| amountInBaseCurrency = expenseAmount * rate | ||
| conversionStatus = "ok" | ||
| } else { | ||
| exchangeRate = 1.0 | ||
| amountInBaseCurrency = expenseAmount | ||
| conversionStatus = "missing_rate" | ||
| } |
There was a problem hiding this comment.
Don't persist a fake base-currency value when the rate is missing.
When the lookup fails, this still writes amountInBaseCurrency = expenseAmount with exchangeRate = 1.0. For any non-base transaction, that records the wrong base amount (e.g. -10 EUR becomes -10 USD) and can silently skew totals. Please fail the intent here, or store a sentinel that the JS layer explicitly excludes from base-currency calculations.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@ios/Stroberi/Intents/LogTransactionIntent.swift` around lines 42 - 52, The
current else branch in LogTransactionIntent.swift assigns exchangeRate = 1.0 and
amountInBaseCurrency = expenseAmount when
IntentDatabaseHelper.fetchExchangeRate(...) returns nil; instead, do NOT persist
a fake base amount — remove the 1.0/expenseAmount assignments and either (A) set
exchangeRate and amountInBaseCurrency to a clear sentinel (e.g., nil or
Double.nan) and keep conversionStatus = "missing_rate" so the JS layer can
exclude it from base-currency calculations, or (B) abort/return a failed intent
response from the intent handler so the transaction is not saved; update the
code around IntentDatabaseHelper.fetchExchangeRate, exchangeRate,
amountInBaseCurrency and conversionStatus accordingly.
| // Intl.NumberFormat throws on unsupported/malformed currency codes. | ||
| // Fall back to a plain representation so a single bad row can't | ||
| // crash the screen — the user can still see and delete the entry. |
There was a problem hiding this comment.
Remove inline comments from TypeScript source
Please remove the inline comments in this block to match repository rules.
As per coding guidelines, **/*.{js,jsx,ts,tsx}: Don't write comments in code.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@lib/format.ts` around lines 15 - 17, Remove the inline comment block that
begins with "Intl.NumberFormat throws on unsupported/malformed currency codes."
and the following two lines in lib/format.ts so the TypeScript source contains
no inline comments per repo rules; leave the code logic unchanged and ensure
only the comment lines are deleted around the Intl.NumberFormat-related note.
| import Foundation | ||
| import SQLite3 |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== Diff plugin vs checked-in iOS helper =="
diff -u \
plugins/intents/IntentDatabaseHelper.swift \
ios/Stroberi/Intents/IntentDatabaseHelper.swift || true
echo
echo "== Inspect copy logic in the config plugin =="
rg -n -C3 'IntentDatabaseHelper\.swift|plugins/intents|Intents' plugins/withAppIntents.js
echo
echo "== Inspect the bridge rationale =="
sed -n '1,20p' ios/Stroberi/Intents/IntentSQLiteBridge.mRepository: stroberi-app/stroberi
Length of output: 7037
Plugin template still uses direct SQLite imports, bypassing the bridge.
The checked-in iOS helper removed the SQLite3 import and moved all database access behind IntentSQLiteBridge to avoid the build-time collision documented in ios/Stroberi/Intents/IntentSQLiteBridge.m (incompatible fts5_api definitions between system and vendored SQLite modules). However, the plugin template at plugins/intents/IntentDatabaseHelper.swift still imports SQLite3 and performs raw sqlite3_* calls. Since plugins/withAppIntents.js copies files from plugins/intents/ into the iOS project during prebuild, the next expo prebuild --clean will overwrite the fixed helper with this stale version and reintroduce the build failure. Update the template to match the bridge-based approach, or consolidate both versions to a shared source.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@plugins/intents/IntentDatabaseHelper.swift` around lines 1 - 2, The template
IntentDatabaseHelper.swift still imports SQLite3 and uses raw sqlite3_* calls;
remove that direct SQLite usage and switch to the IntentSQLiteBridge API (call
its exposed methods instead of
sqlite3_open/sqlite3_prepare_v2/sqlite3_step/sqlite3_finalize/sqlite3_close) so
the plugin uses the same bridged implementation as the iOS helper; update the
template functions (e.g., any open/prepare/execute/query helpers) to invoke
IntentSQLiteBridge methods and/or consolidate this file with the fixed helper
used by the app (so plugins/withAppIntents.js copies the bridge-based source),
ensuring no direct SQLite3 import or sqlite3_* symbols remain in
IntentDatabaseHelper.swift.
Summary by CodeRabbit
New Features
Bug Fixes
Chores